1. 函数组件:React 的基石
在现代 React 中,我们几乎只使用函数组件。它就是一个返回 UI 描述(JSX)的 JavaScript 函数。
一个最简单的组件:
// src/components/Greeting.jsx
// 这就是一个 React 组件
function Greeting() {
// 它返回了 JSX,看起来像 HTML,但它是 JavaScript
return <h1>Hello, React!</h1>;
}
// 导出这个组件,以便在其他地方使用
export default Greeting;
在另一个组件中使用它:
// src/App.jsx
import Greeting from './components/Greeting';
function App() {
return (
<div>
<p>Welcome to my app.</p>
<Greeting /> {/* 像使用 HTML 标签一样使用你的组件 */}
</div>
);
}
export default App;
关键点:
- 大写字母开头:组件名必须是
Greeting
而不是greeting
。这是 React 区分原生 HTML 标签 (如div
) 和自定义组件的规则。 - 单一根元素:组件返回的 JSX 必须被一个父元素包裹。如果不想添加多余的
div
,可以使用 Fragment:<>...</>
。
2. 使用 useState
管理组件状态
组件光有静态内容是不够的,它需要有自己的数据(状态),并且在数据变化时更新视图。这就是 useState
的作用,它相当于 Vue 的 ref
。
useState
是一个 Hook,你可以把它理解为“钩入” React 功能的特殊函数。
一个计数器例子:
import { useState } from 'react'; // 必须从 'react' 导入
function Counter() {
// 1. 调用 useState,传入初始值 0
// 2. 它返回一个数组,包含两项:
// - count: 当前的状态值 (只读)
// - setCount: 更新这个状态的唯一函数
const [count, setCount] = useState(0);
function handleClick() {
// 错误的做法!不能直接修改状态
// count++;
// 正确的做法:调用 setter 函数,传入新的值
setCount(count + 1);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
与 Vue 对比的核心差异(非常重要!):
- 不可变性 (Immutability):在 Vue 中,你可能会写
count.value++
。在 React 中,你绝不能直接修改count
。你必须通过setCount
函数来“请求”一次更新。React 会用你提供的新值替换旧的值,然后重新渲染组件。 - 更新对象/数组状态:因为不可变性,更新复杂状态时,你需要创建一个新的对象或数组。
const [user, setUser] = useState({ name: 'Alice', age: 30 });
function handleNameChange() {
// 错误: setUser({ name: 'Bob' }); // 这会丢失 age 属性
// 正确: 使用展开语法(...)复制旧属性,再覆盖要修改的属性
setUser({ ...user, name: 'Bob' });
}
3. 使用 props
传递数据
这和 Vue 的 props
思想完全一致:父组件向子组件传递数据。
父组件 App.jsx
:
import UserProfile from './UserProfile';
function App() {
const userInfo = {
name: 'Chris',
avatarUrl: 'https://i.imgur.com/yXOvdOSs.jpg'
};
return (
<div>
{/* 像 HTML 属性一样传递 props */}
<UserProfile user={userInfo} posts={15} />
</div>
);
}
子组件 UserProfile.jsx
:
// props 是一个对象,包含了父组件传递的所有属性
// 通常我们用解构赋值直接获取需要的 props
function UserProfile({ user, posts }) {
return (
<div>
<img src={user.avatarUrl} alt={user.name} />
<h2>{user.name}</h2>
<p>Posts: {posts}</p>
</div>
);
}
子组件通知父组件 (状态提升)
在 Vue 中,你会用 $emit
。在 React 中,你直接将一个函数作为 prop 传递下去。
// 父组件 Parent.jsx
function Parent() {
const [name, setName] = useState('World');
// 这个函数将被传递给子组件
const handleChildClick = (newName) => {
setName(newName);
}
return (
<div>
<h1>Hello, {name}</h1>
{/* 将函数作为 prop 传递 */}
<Child onUpdateName={handleChildClick} />
</div>
);
}
// 子组件 Child.jsx
function Child({ onUpdateName }) {
return (
// 当按钮被点击时,调用从 props 收到的函数
<button onClick={() => onUpdateName('React')}>
Change Name to React
</button>
);
}
这个“状态提升”模式是 React 中实现子到父通信的标准做法。
4. 使用 useEffect
处理副作用
useEffect
是另一个极其重要的 Hook。它用于处理副作用 (Side Effects),即组件渲染之外的任何操作,比如:
- API 请求
- 设置定时器或订阅
- 手动操作 DOM
它完美地整合了 Vue 的 onMounted
, onUpdated
, onUnmounted
和 watch
。
useEffect
的结构: useEffect(callback, [dependencies])
callback
: 你要执行的副作用代码。[dependencies]
(依赖数组): 这是useEffect
的灵魂,它告诉 React 何时重新运行你的副作用代码。
三种主要用法:
-
模拟
onMounted
: 获取数据useEffect(() => { // 这个函数只在组件第一次渲染后执行 console.log('Component has mounted!'); fetch('https://api.example.com/data') .then(res => res.json()) .then(data => setData(data)); }, []); // <-- 依赖数组为空,表示不依赖任何 props 或 state,所以只运行一次
-
模拟
watch
: 依赖项变化时执行useEffect(() => { // 首次渲染后会执行一次 // 之后,只有当 userId 发生变化时,才会再次执行 console.log(`User ID changed to: ${userId}`); fetch(`https://api.example.com/users/${userId}`) .then(res => res.json()) .then(data => setUserData(data)); }, [userId]); // <-- 依赖数组里有 userId
-
模拟
onUnmounted
: 清理副作用useEffect
的回调函数可以返回一个清理函数。这个函数会在组件卸载前,或者在下一次 effect 运行前被调用。useEffect(() => { const timerId = setInterval(() => { console.log('tick'); }, 1000); // 返回一个清理函数 return () => { console.log('Clearing interval'); clearInterval(timerId); // 组件卸载时清除定时器,防止内存泄漏 }; }, []); // 空依赖数组,所以清理函数只在卸载时运行
5. 条件渲染与列表渲染
这部分充分体现了 React “All in JS” 的哲学。
-
条件渲染 (替代
v-if
): 使用 JavaScript 的原生条件表达式。function UserStatus({ isLoggedIn }) { // 1. 使用三元运算符 return isLoggedIn ? <p>Welcome back!</p> : <p>Please log in.</p>; // 2. 使用 && 短路与 (只在条件为真时渲染) return ( <div> <h2>My App</h2> {isLoggedIn && <button>Logout</button>} </div> ); }
-
列表渲染 (替代
v-for
): 使用 JavaScript 的.map()
方法。function TodoList({ todos }) { return ( <ul> {todos.map(todo => ( // 关键:必须为列表中的每一项提供一个独一无二的 `key` prop // 这和 Vue 的 :key 作用完全一样,用于高效的 DOM diff <li key={todo.id}> {todo.text} </li> ))} </ul> ); }
6. 表单处理 (受控组件)
这是和 Vue 的 v-model
差异较大的地方。React 推荐使用受控组件 (Controlled Components),即表单元素的值完全由 React 的 state 控制。
一个受控的输入框:
import { useState } from 'react';
function NameForm() {
const [name, setName] = useState('');
const handleSubmit = (event) => {
event.preventDefault(); // 阻止表单默认的提交行为
alert(`A name was submitted: ${name}`);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
{/*
数据流是单向的:
1. state -> input: `value` 属性绑定了 `name` state。
2. input -> state: `onChange` 事件触发 `setName`,用输入框的当前值更新 state。
*/}
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
</label>
<button type="submit">Submit</button>
</form>
);
}
这看起来比 v-model
繁琐,但它的优点是数据流非常明确。任何时候,组件的 state 都是表单数据的“唯一数据源”,这使得调试和管理状态变得更简单。